﻿using Manox.Libs.Networking;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Manox.Libs.ODB
{
    /// <summary>
    /// Object Database Synchronizer.
    /// </summary>
    [RequireComponent(typeof(ObjectDatabaseManager))]
    public abstract class ObjectDatabaseSynchronizer : MonoBehaviour
    {
        /// <summary>
        /// List item.
        /// </summary>
        public class ListItem
        {
            /// <summary>List.</summary>
            public ObjectDatabaseListBase List { get; private set; }
            /// <summary>Index.</summary>
            public int Index { get; private set; }


            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="list"></param>
            /// <param name="index"></param>
            public ListItem(ObjectDatabaseListBase list, int index)
            {
                this.List = list;
                this.Index = index;
            }
        }

        /// <summary>Object database.</summary>        
        public ObjectDatabaseManager ODBManager;
        /// <summary>Unbind properties.</summary>
        private SortedDictionary<Guid, List<ObjectDatabasePropertyBase>> UnbindProperties;
        /// <summary>Unbind list items.</summary>
        private SortedDictionary<Guid, List<ListItem>> UnbindListItems;
        /// <summary>Unfinished objects.</summary>
        private SortedDictionary<Guid, ObjectDatabaseItem> UnfinishedObjects;


        /// <summary>
        /// Constructor.
        /// </summary>
        public ObjectDatabaseSynchronizer()
        {
            this.UnbindProperties = new SortedDictionary<Guid, List<ObjectDatabasePropertyBase>>();
            this.UnbindListItems = new SortedDictionary<Guid, List<ListItem>>();
            this.UnfinishedObjects = new SortedDictionary<Guid, ObjectDatabaseItem>();
        }

        /// <summary>
        /// Awake.
        /// </summary>
        public virtual void Awake()
        {
            NetworkMessageTranslator.Instance.RegisterType<ODBSynchronizationMessage>();
        }

        /// <summary>
        /// Set ODB manager.
        /// </summary>
        /// <param name="odbManager"></param>        
        public void SetObjectDatabase(ObjectDatabaseManager odbManager)
        {
            this.ODBManager = odbManager;

            if (this.ODBManager != null)
            {
                this.ODBManager.ItemAdded += this.ObjectDatabaseItemAdded;
                this.ODBManager.ItemChanged += this.ObjectDatabaseItemChanged;
            }
        }

        /// <summary>
        /// Dispose.
        /// </summary>
        public virtual void OnDestroy()
        {
            if (this.ODBManager != null)
            {
                this.ODBManager.ItemChanged -= this.ObjectDatabaseItemChanged;
                this.ODBManager.ItemAdded -= this.ObjectDatabaseItemAdded;

                this.ODBManager = null;
            }
        }

        /// <summary>
        /// Fixed update.
        /// </summary>
        public virtual void FixedUpdate()
        {
        }

        /// <summary>
        /// Synchronize add item.
        /// </summary>
        /// <param name="eventArgs"></param>
        private void SynchronizeAddItem(ItemWithPropertiesAddedEventArgs eventArgs)
        {
            ObjectDatabaseItem odbItem = eventArgs.ItemAddedEventArgs.NewItem;

            if (odbItem == null)
            {
                odbItem = null;
            }

            if (odbItem is ObjectDatabaseObject)
                this.SynchronizeAddItemObject(eventArgs);
            if (odbItem is ObjectDatabaseListBase)
                this.SynchronizeAddItemList(eventArgs);
                        
            this.BindObject(odbItem);

            if (odbItem.IsFinished())
                this.ODBManager.AddObject(this, odbItem);
            else
                lock (this.UnfinishedObjects)
                    this.UnfinishedObjects.Add(odbItem.Id, odbItem);
        }

        /// <summary>
        /// Add item object.
        /// </summary>
        /// <param name="eventArgs"></param>
        private void SynchronizeAddItemObject(ItemWithPropertiesAddedEventArgs eventArgs)
        {
            ObjectDatabaseObject odbObject = eventArgs.ItemAddedEventArgs.NewItem as ObjectDatabaseObject;

            foreach (ObjectDatabasePropertyValueChangedEventArgs itemEventArgs in eventArgs.Properties)
                this.SetProperty(odbObject, itemEventArgs);            
        }

        /// <summary>
        /// Add item list.
        /// </summary>
        /// <param name="eventArgs"></param>
        private void SynchronizeAddItemList(ItemWithPropertiesAddedEventArgs eventArgs)
        {
            ObjectDatabaseListBase odbObject = eventArgs.ItemAddedEventArgs.NewItem as ObjectDatabaseListBase;

            foreach (ObjectDatabaseListChangedEventArgs itemEventArgs in eventArgs.Properties)
            {
                if (itemEventArgs.AffectedItem is ObjectDatabaseItemProxy)
                {
                    Guid objectId = (itemEventArgs.AffectedItem as ObjectDatabaseItemProxy).Id;

                    ObjectDatabaseItem tempItem = this.GetObjectInODBOrUnfinished(objectId);

                    if (tempItem != null)
                        odbObject.Add(this, tempItem);
                    else
                        lock (this.UnbindListItems)
                        {
                            if (!this.UnbindListItems.ContainsKey(objectId))
                                this.UnbindListItems.Add(objectId, new List<ListItem>());

                            this.UnbindListItems[objectId].Add(new ListItem(odbObject, odbObject.Count));

                            odbObject.Add(this, null);
                        }
                }
                else
                    odbObject.Add(this, itemEventArgs.AffectedItem);
            }                              
        }

        /// <summary>
        /// Synchronize object property.
        /// </summary>
        /// <param name="eventArgs"></param>
        private void SynchronizeObjectProperty(ObjectDatabaseObjectPropertyValueChangedEventArgs eventArgs)
        {
            ObjectDatabaseObject item = this.ODBManager[eventArgs.DatabaseObjectId] as ObjectDatabaseObject;

            if (item != null)
                this.SetProperty(item, eventArgs.PropertyEventArgs);            
        }

        /// <summary>
        /// Set property.
        /// </summary>
        /// <param name=""></param>
        /// <param name=""></param>
        /// <param name="propertyEvent"></param>
        private void SetProperty(ObjectDatabaseObject odbObject, ObjectDatabasePropertyValueChangedEventArgs propertyEvent)
        {
            ObjectDatabasePropertyBase propertyBase = odbObject.GetProperty(propertyEvent.PropertyName);
            
            if (propertyEvent.NewValue is ObjectDatabaseItemProxy)
            {
                Guid objectId = (propertyEvent.NewValue as ObjectDatabaseItemProxy).Id;

                ObjectDatabaseItem tempItem = this.GetObjectInODBOrUnfinished(objectId);

                if (tempItem != null)
                    propertyBase.SetObject(this, tempItem);
                else
                    lock (this.UnbindProperties)
                    {
                        if (!this.UnbindProperties.ContainsKey(objectId))
                            this.UnbindProperties.Add(objectId, new List<ObjectDatabasePropertyBase>());

                        this.UnbindProperties[objectId].Add(propertyBase);

                        //propertyBase.SetObject(this, (propertyEvent.NewValue));
                    }
            }
            else
                propertyBase.SetObject(this, propertyEvent.NewValue);
        }        

        /// <summary>
        /// Synchronize object list.
        /// </summary>
        private void SynchronizeListObject(ObjectDatabaseListChangedEventArgs eventArgs)
        {
            ObjectDatabaseListBase item = this.ODBManager[eventArgs.DatabaseObjectId] as ObjectDatabaseListBase;

            if (eventArgs.AffectedItem is ObjectDatabaseItemProxy)
                eventArgs.AffectedItem = this.ODBManager[(eventArgs.AffectedItem as ObjectDatabaseItemProxy).Id];

            switch (eventArgs.EventType)
            {
                case ListChangedEventType.Clear:
                    item.Clear(this);
                    break;
                case ListChangedEventType.ItemAdded:
                    item.Add(this, eventArgs.AffectedItem);
                    break;
                case ListChangedEventType.ItemRemoved:
                    item.Remove(this, eventArgs.AffectedItem);
                    break;
            }
        }

        /// <summary>
        /// Bind object.
        /// </summary>
        /// <param name="item"></param>
        private void BindObject(ObjectDatabaseItem item)
        {
            lock (this.UnbindProperties)
                if (this.UnbindProperties.ContainsKey(item.Id))
                {
                    List<ObjectDatabasePropertyBase> properties = this.UnbindProperties[item.Id];
                    this.UnbindProperties.Remove(item.Id);

                    foreach (ObjectDatabasePropertyBase property in properties)
                        property.SetObject(this, item);
                }

            lock (this.UnbindListItems)
                if (this.UnbindListItems.ContainsKey(item.Id))
                {
                    List<ListItem> listItems = this.UnbindListItems[item.Id];
                    this.UnbindListItems.Remove(item.Id);

                    foreach (ListItem listItem in listItems)
                        listItem.List.SetItemBase(this, listItem.Index, item);
                }

            lock (this.UnfinishedObjects)
            {
                HashSet<Guid> removeGuids = new HashSet<Guid>();

                foreach (KeyValuePair<Guid, ObjectDatabaseItem> itemObject in this.UnfinishedObjects)
                    if (itemObject.Value.IsFinished())
                    {
                        removeGuids.Add(itemObject.Key);

                        this.ODBManager.AddObject(this, itemObject.Value);
                    }

                foreach (Guid itemId in removeGuids)
                    this.UnfinishedObjects.Remove(itemId);
            }
        }

        /// <summary>
        /// Get object in ODB or unfinished collection.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private ObjectDatabaseItem GetObjectInODBOrUnfinished(Guid id)
        {
            ObjectDatabaseItem output = this.ODBManager[id];

            if (output == null)
                lock (this.UnfinishedObjects)
                    if (this.UnfinishedObjects.ContainsKey(id))
                        output = this.UnfinishedObjects[id];

            return output;
        }

        /// <summary>
        /// Object database item added.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="eventArgs"></param>
        private void ObjectDatabaseItemAdded(object sender, ItemAddedEventArgs eventArgs)
        {
            if (!(sender is ObjectDatabaseSynchronizer))
            {
                ItemWithPropertiesAddedEventArgs itemWithPropertiesEventArgs = new ItemWithPropertiesAddedEventArgs();
                itemWithPropertiesEventArgs.ItemAddedEventArgs = eventArgs;
                itemWithPropertiesEventArgs.Properties = eventArgs.NewItem.InitializingEventArgs();

                ODBSynchronizationMessage newMessage = new ODBSynchronizationMessage();
                newMessage.EventArgs = itemWithPropertiesEventArgs;                

                this.SendUpdateMessage(newMessage);
            }
        }

        /// <summary>
        /// Object database item changed.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="eventArgs"></param>
        private void ObjectDatabaseItemChanged(object sender, ObjectDatabaseEventArgs eventArgs)
        {
            if (!(sender is ObjectDatabaseSynchronizer))
            {
                ODBSynchronizationMessage newMessage = new ODBSynchronizationMessage();
                newMessage.EventArgs = eventArgs;

                this.SendUpdateMessage(newMessage);
            }
        }

        /// <summary>
        /// Send update message.
        /// </summary>
        /// <param name="message"></param>
        public abstract void SendUpdateMessage(ODBSynchronizationMessage message);

        /// <summary>
        /// Process data.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="connectionId"></param>
        /// <param name="message">
        public virtual void ProcessNetworkMessage(object sender, NetworkConnectionId connectionId, ODBSynchronizationMessage message)
        {
            if (message.EventArgs is ItemWithPropertiesAddedEventArgs)
                this.SynchronizeAddItem(message.EventArgs as ItemWithPropertiesAddedEventArgs);
            else if (message.EventArgs is ObjectDatabaseObjectPropertyValueChangedEventArgs)
                this.SynchronizeObjectProperty(message.EventArgs as ObjectDatabaseObjectPropertyValueChangedEventArgs);
            else if (message.EventArgs is ObjectDatabaseListChangedEventArgs)
                this.SynchronizeListObject(message.EventArgs as ObjectDatabaseListChangedEventArgs);
        }
    }


    /// <summary>
    /// Item added event args.
    /// </summary>
    public class ItemWithPropertiesAddedEventArgs : ObjectDatabaseEventArgs
    {
        /// <summary>Item added event args.</summary>
        public ItemAddedEventArgs ItemAddedEventArgs;
        /// <summary>Properties.</summary>
        public List<ObjectDatabaseEventArgs> Properties;



        /// <summary>
        /// Serialize.
        /// </summary>
        /// <param name="writer"></param>
        public override void Serialize(SerializeWriter writer)
        {
            base.Serialize(writer);
            
            this.ItemAddedEventArgs.Serialize(writer);

            writer.Write(this.Properties.Count);

            foreach (ObjectDatabaseEventArgs propertyEvent in this.Properties)
            {
                byte typeId = ODBSynchronizationMessageTranslator.Instance.GetTypeId(propertyEvent.GetType());

                writer.Write(typeId);
                
                propertyEvent.Serialize(writer);
            }
        }

        /// <summary>
        /// Deserialize.
        /// </summary>
        /// <param name="reader"></param>
        public override void Deserialize(SerializeReader reader)
        {
            base.Deserialize(reader);

            if (this.ItemAddedEventArgs == null)
                this.ItemAddedEventArgs = new ItemAddedEventArgs();

            this.ItemAddedEventArgs.Deserialize(reader);

            int count = reader.ReadInt32();

            if (this.Properties == null)
                this.Properties = new List<ObjectDatabaseEventArgs>();
            else
                this.Properties.Clear();

            for (int i = 0; i < count; i++)
            {
                byte type = reader.ReadByte();

                ObjectDatabaseEventArgs propertyEvents = ODBSynchronizationMessageTranslator.Instance.GetInstance(type) as ObjectDatabaseEventArgs;

                if (propertyEvents == null)
                {
                    Debug.Log("Neco : " + this.ItemAddedEventArgs.NewItem);
                }

                propertyEvents.Deserialize(reader);

                this.Properties.Add(propertyEvents);
            }
        }
    }
}
